<?php
namespace classlib\session;
class secure extends \SessionHandler {
    protected $key;
    /**
     * 初始化构造信息
     * @throws \RuntimeException
     */
    public function __construct() {
        if (!extension_loaded('openssl')) {
            throw new \RuntimeException(sprintf('You need the OpenSSL extension to use %s', __CLASS__));
        }
        if (!extension_loaded('mbstring')) {
            throw new \RuntimeException(sprintf('You need the Multibytes extension to use %s', __CLASS__));
        }
    }

    /**
     * 初始化Session资料信息
     * @param string $save_path
     * @param string $session_name
     * @return bool
     */
    public function open($save_path, $session_name) {
        $this->key = $this->get_key('key_' . $session_name);
        return parent::open($save_path, $session_name);
    }

    /**
     * 读取数据，并且解密数据信息
     * @param string $id
     */
    public function read($id) {
        $data = parent::read($id);
        return empty($data)? '':$this->decrypt($data, $this->key);
    }

    /**
     * 数据写入加密
     * @param string $id
     * @param string $data
     */
    public function write($id, $data) {
        return parent::write($id, $this->encrypt($data, $this->key));
    }

    /**
     * Encrypt and authenticate
     *
     * @param string $data
     * @param string $key
     * @return string
     */
    protected function encrypt($data, $key) {
        $iv     = $this->rand_key(16); // AES block size in CBC mode
        $cipher = openssl_encrypt($data, 'AES-256-CBC', mb_substr($key, 0, 32, '8bit'), OPENSSL_RAW_DATA, $iv);
        $hmac   = hash_hmac('SHA256', $iv . $cipher, mb_substr($key, 32, null, '8bit'), true);
        return $hmac.$iv.$cipher;
    }

    /**
     * Authenticate and decrypt
     * @param string $data
     * @param string $key
     * @return string
     */
    protected function decrypt($data, $key) {
        $hmac    = mb_substr($data, 0, 32, '8bit');
        $iv      = mb_substr($data, 32, 16, '8bit');
        $cipher  = mb_substr($data, 48, null, '8bit');
        $hmacNew = hash_hmac('SHA256', $iv . $cipher, mb_substr($key, 32, null, '8bit'), true);
        if (!$this->hash_equals($hmac, $hmacNew)) {
            throw new \RuntimeException('Authentication failed');
        }
        return openssl_decrypt($cipher, 'AES-256-CBC', mb_substr($key, 0, 32, '8bit'), OPENSSL_RAW_DATA, $iv);
    }

    /**
     * Get the encryption and authentication keys from cookie
     * @param string $name
     * @return string
     */
    protected function get_key($name) {
        if (!isset($_COOKIE[$name]) || empty($_COOKIE[$name])) {
            $skey  = base64_encode($this->rand_key(16)); 
            $args  = session_get_cookie_params();
            $expire= ($args['lifetime'] > 0)? time()+$args['lifetime']:0;
            setcookie($name, $skey, $expire, '/', $args['domain'], $args['secure'], $args['httponly']);
        } else {
            $skey  = base64_decode($_COOKIE[$name]);
        }
        return $skey;
    }
    
    /**
     * 生成随机加密串信息
     * @param int $len
     * @return string
     */
    protected function rand_key($len) {
    	if (function_exists('random_bytes')) {
    		return random_bytes($len);
    	} else {
    		$bstr = (string)microtime(true);
    		$bstr = (strlen(($bstr)) > $len)? substr($bstr, 0, $len):str_pad($bstr, $len, 'A');
    		return $bstr;
    	}
    }

    /**
     * 比较两个字符是否Hash一致
     * @param string $expected
     * @param string $actual
     * @return bool
     */
    protected function hash_equals($expected, $actual) {
        $expected = (string) $expected;
        $actual   = (string) $actual;
        if (function_exists('hash_equals')) {
            return hash_equals($expected, $actual);
        }
        $lenExpected = mb_strlen($expected, '8bit');
        $lenActual   = mb_strlen($actual, '8bit');
        $len         = min($lenExpected, $lenActual);
        $result      = 0;
        for ($i = 0; $i < $len; $i++) {
            $result |= ord($expected[$i]) ^ ord($actual[$i]);
        }
        $result |= $lenExpected ^ $lenActual;
        return ($result === 0);
    }
    
    /**
     * 设置Session start
     * @param string $path
     */
    public static function start($path='') {
    	session_set_save_handler(new secure(), true);
    	if ($path && is_dir($path)) {
    		session_save_path($path);
    	}
    	session_start();
    }
}
